#nullable disable
using System.Globalization;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.Extensions.Caching.Memory;

namespace Crux.Core.EFCore;
/// <summary>
/// QueryRepository
/// </summary>
public class QueryRepository<TContext>(
    IUnitOfWork<TContext> unitOfWork,
    IMemoryCache memoryCache,
    IHashProvider hashProvider)
    : IDisposableObservable, IAsyncDisposableObservable,
        IQueryRepository<TContext>
    where TContext : DbContext
{

    #region private fields
    private readonly IUnitOfWork<TContext> unitOfWork = unitOfWork
                                                        ?? throw new ArgumentNullException(nameof(unitOfWork));
    private readonly IMemoryCache memoryCache = memoryCache
                                                ?? throw new ArgumentNullException(nameof(memoryCache));
    private readonly IHashProvider hashProvider = hashProvider
                                                  ?? throw new ArgumentNullException(nameof(hashProvider));

    #endregion  private fields

    #region implements IQueryRepository
    /// <inheritdoc/>
    public bool IsDisposed { get; private set; }
    /// <inheritdoc/>
    public TContext Context => this.unitOfWork.Context;
    /// <inheritdoc/>
    public bool Any<T>(
        Expression<Func<T, bool>> predicate,
        bool ignoreFilter = false)
        where T : class
        => Query<T>(ignoreFilter: ignoreFilter).Where(predicate).Any();
    /// <inheritdoc/>
    public async Task<bool> AnyAsync<T>(
        Expression<Func<T, bool>> predicate,
        bool ignoreFilter = false,
        CancellationToken cancellationToken = default)
        where T : class
        => await Query<T>(ignoreFilter: ignoreFilter).Where(predicate).AnyAsync(cancellationToken).ConfigureAwait(false);
    /// <inheritdoc/>
    public T FirstOrDefault<T>(Expression<Func<T, bool>> predicate, bool ignoreFilter = false) where T : class
        => Query<T>(ignoreFilter: ignoreFilter).Where(predicate).FirstOrDefault();
    /// <inheritdoc/>
    public async Task<T> FirstOrDefaultAsync<T>(Expression<Func<T, bool>> predicate, bool ignoreFilter = false, CancellationToken cancellationToken = default) where T : class
        => await Query<T>(ignoreFilter: ignoreFilter).Where(predicate).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
    /// <inheritdoc/>
    public int Count<T>(Expression<Func<T, bool>> predicate, bool ignoreFilter = false) where T : class
        => this.Query<T>(true, ignoreFilter: ignoreFilter).Where(predicate).Count();
    /// <inheritdoc/>
    public async Task<int> CountAsync<T>(Expression<Func<T, bool>> predicate, bool ignoreFilter = false, CancellationToken cancellationToken = default) where T : class
        => await this.Query<T>(true, ignoreFilter: ignoreFilter).Where(predicate).CountAsync(cancellationToken).ConfigureAwait(false);
    /// <inheritdoc/>
    public long LongCount<T>(Expression<Func<T, bool>> predicate, bool ignoreFilter = false) where T : class
        => this.Query<T>(true, ignoreFilter: ignoreFilter).Where(predicate).LongCount();
    /// <inheritdoc/>
    public async Task<long> LongCountAsync<T>(Expression<Func<T, bool>> predicate, bool ignoreFilter = false, CancellationToken cancellationToken = default) where T : class
        => await this.Query<T>(true, ignoreFilter: ignoreFilter).Where(predicate).LongCountAsync(cancellationToken).ConfigureAwait(false);
    /// <inheritdoc/>
    public void Dispose()
    {
        if (this.IsDisposed)
        {
            return;
        }

        this.IsDisposed = true;
        this.unitOfWork.Dispose();
    }
    /// <inheritdoc/>
    public ValueTask DisposeAsync()
    {
        return this.unitOfWork.DisposeAsync();
    }
    /// <inheritdoc/>
    public IQueryable<T> Query<T>(bool noTracking = true, bool ignoreFilter = false) where T : class
    {
        var queryable = noTracking switch
        {
            true => this.Set<T>().AsNoTracking(),
            _ => this.Set<T>()
        };

        if (ignoreFilter)
            queryable = queryable.IgnoreQueryFilters();

        return queryable;
    }
    /// <inheritdoc/>
    public IQueryable<TEntity> FromSqlRaw<TEntity>(string sql, params object[] parameters) where TEntity : class
        => this.Context.Set<TEntity>().FromSqlRaw(sql, parameters);
    /// <inheritdoc/>
    public IQueryable<TEntity> SqlQueryRaw<TEntity>(string sql, params object[] parameters)
    {
        return this.Context.Database.SqlQueryRaw<TEntity>(sql, parameters);
    }
    /// <inheritdoc/>
    public async Task<Pageable<T>> GetByPageAsync<T>(
        IEnumerable<OrderExpression<T>> orderExpressions,
        Expression<Func<T, bool>> where = null,
        PageOption option = default,
        CancellationToken cancellationToken = default)
        where T : class
     => await Task.Run(() => this.GetByPage(orderExpressions, where, option), cancellationToken).ConfigureAwait(false);
    /// <inheritdoc/>
    private IOrderedQueryable<T> GetOrderedQuery<T>(
        IEnumerable<OrderExpression<T>> orderExpressions,
        IQueryable<T> query)
    {
        Requires.NotNullOrEmpty(orderExpressions, nameof(orderExpressions));

        var firstExpression = orderExpressions.FirstOrDefault();
        if (firstExpression.Expression is null)
            throw new ArgumentNullException(nameof(firstExpression.Expression));

        IOrderedQueryable<T> orderQuery = firstExpression.Symbol switch
        {
            OrderSymbol.Asc => query.OrderBy(firstExpression.Expression),
            _ => query.OrderByDescending(firstExpression.Expression)
        };

        foreach (var orderExpression in orderExpressions)
        {
            if (firstExpression == orderExpression) continue;
            if (orderExpression.Expression is null)
                throw new ArgumentNullException(nameof(orderExpression.Expression));

            orderQuery = orderExpression.Symbol switch
            {
                OrderSymbol.Asc => orderQuery.ThenBy(firstExpression.Expression),
                _ => orderQuery.ThenByDescending(firstExpression.Expression)
            };
        }

        return orderQuery;
    }
    /// <inheritdoc/>
    public Pageable<T> GetByPage<T>(
        IEnumerable<OrderExpression<T>> orderExpressions,
        Expression<Func<T, bool>> where = null,
        PageOption option = default)
        where T : class
    {
        Requires.NotNull(orderExpressions, nameof(orderExpressions));

        var query = where is null ? this.Query<T>() : this.Query<T>().Where(where);

        query = this.GetOrderedQuery(orderExpressions, query);
        if (option.PageSize.HasValue &&
            option.PageIndex.HasValue)
        {
            if (0 == option.PageSize)
            {
                query = query.Where(p => false);
                return new Pageable<T>(query, option, 0);
            }
            else
            {
                int totalCount = query.Count();
                query = query.Skip((option.PageIndex.Value - 1) * option.PageSize.Value).Take(option.PageSize.Value);
                return new Pageable<T>(query, option, totalCount);
            }
        }
        else
        {
            int totalCount = query.Count();

            return new Pageable<T>(query, 1, totalCount, totalCount);
        }
    }
    /// <inheritdoc/>
    public DbSet<T> Set<T>() where T : class
        => this.Context.Set<T>();

    /// <inheritdoc/>
    public T Find<T, TPrimary>(TPrimary primary) where T : class, IGenericEntity<TPrimary>
    {
        var key = hashProvider.ComputeHash($"CacheCompileQuery:{typeof(T).FullName}");

        Func<ICacheEntry, Func<DbContext, TPrimary, T>> factory = (_) =>
        {
            return EF.CompileQuery((DbContext context, TPrimary id) => Set<T>().AsNoTracking().FirstOrDefault(c => c.Id.Equals(id)));
        };

        var cached = memoryCache.GetOrCreate(key, factory);

        return cached(Context, primary);
    }

    /// <inheritdoc/>
    public T Find<T>(object id) where T : class
    {
        if (id is null)
        {
            throw new ArgumentNullException(nameof(id));
        }

        IEntityType entityType = this.Context.Model.FindEntityType(typeof(T));

        string primaryKeyName = entityType.FindPrimaryKey().Properties.Select(p => p.Name).FirstOrDefault();
        Type primaryKeyType = entityType.FindPrimaryKey().Properties.Select(p => p.ClrType).FirstOrDefault();

        if (primaryKeyName is null || primaryKeyType is null)
        {
            throw new ArgumentException("Entity does not have any primary key defined", nameof(id));
        }

        object primaryKeyValue = null;

        try
        {
            primaryKeyValue = Convert.ChangeType(id, primaryKeyType, CultureInfo.InvariantCulture);
        }
        catch (Exception)
        {
            throw new ArgumentException($"You can not assign a value of type {id.GetType()} to a property of type {primaryKeyType}");
        }

        ParameterExpression pe = Expression.Parameter(typeof(T), "entity");
        MemberExpression me = Expression.Property(pe, primaryKeyName);
        ConstantExpression constant = Expression.Constant(primaryKeyValue, primaryKeyType);
        BinaryExpression body = Expression.Equal(me, constant);
        Expression<Func<T, bool>> expressionTree = Expression.Lambda<Func<T, bool>>(body, new[] { pe });

        return this.Context.Set<T>().FirstOrDefault(expressionTree);
    }
    /// <inheritdoc/>
    public IQueryable<T> Query<T>(Expression<Func<T, bool>> expression, bool noTracking = true, bool ignoreFilter = false) where T : class
    {
        return this.Query<T>(noTracking, ignoreFilter).Where(expression);
    }
    /// <inheritdoc/>
    public IQueryRepository<TContext> SetTimeout(int? timeout)
    {
        this.Context.Database.SetCommandTimeout(timeout);
        return this;
    }

    #endregion implements IQueryRepository
}